use crate::*;
use std::borrow::Cow;
use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};

mod font_alignment;
pub use font_alignment::FontAlignment;

mod font_weight;
pub use font_weight::FontWeight;

mod font_family;
pub use font_family::FontFamily;

mod font_line;
pub use font_line::FontLine;

mod line_height;
pub use line_height::{IntoOptionLineHeight, LineHeight};

/// Styling to manage font.
#[derive(Default, Clone)]
pub struct Font<'a> {
    /// Static css properties used by zoon.
    static_css_props: StaticCSSProps<'a>,
    /// Customizable css properties which can be added.
    dynamic_css_props: DynamicCSSProps,
}

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumIter, IntoStaticStr)]
#[strum(serialize_all = "kebab-case")]
enum StyleName {
    FontWeight,
    Color,
    FontSize,
    LineHeight,
    FontStyle,
    WhiteSpace,
    WordBreak,
    TextAlign,
    FontFamily,
    LetterSpacing,
}

impl Font<'static> {
    pub fn with_signal_self(
        font: impl Signal<Item = impl Into<Option<Self>>> + Unpin + 'static,
    ) -> Self {
        let mut this = Self::default();
        let font = font.map(|font| font.into()).broadcast();
        for style_name in StyleName::iter() {
            this.dynamic_css_props.insert(
                <&str>::from(style_name).into(),
                font.signal_ref(move |font: &Option<Font>| {
                    if let Some(font) = font {
                        if let Some(value) = font.static_css_props.0.get(style_name.into()) {
                            return always(Some(value.clone())).boxed_local();
                        }
                        if let Some(value) = font.dynamic_css_props.get(style_name.into()) {
                            return value.signal_cloned().boxed_local();
                        }
                    }
                    always(None).boxed_local()
                })
                .flatten()
                .boxed_local()
                .broadcast(),
            );
        }
        this
    }
}

impl<'a> Font<'a> {
    pub fn new() -> Self {
        Self::default()
    }

    /// Define the font weight.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let button = Button::new()
    ///     .s(Font::new().weight(FontWeight::Bold))
    ///     .label("Click me");
    /// ```
    /// It can also be set manually.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let button = Button::new()
    ///     .s(Font::new().weight(FontWeight::Number(950)))
    ///     .label("Click me");
    /// ```
    pub fn weight(mut self, weight: FontWeight) -> Self {
        self.static_css_props
            .insert(StyleName::FontWeight.into(), weight.number().into_cow_str());
        self
    }

    /// Define the font weight depending of signal's state..
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let (hovered, hover_signal) = Mutable::new_and_signal(false);
    /// let button = Button::new()
    ///     .s(Font::new()
    ///         .weight_signal(hover_signal.map_bool(|| FontWeight::Bold, || FontWeight::Light)))
    ///     .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
    ///     .label("Hover me");
    /// ```
    pub fn weight_signal(
        mut self,
        weight: impl Signal<Item = impl Into<Option<FontWeight>>> + Unpin + 'static,
    ) -> Self {
        let weight = weight.map(|weight| weight.into().map(|weight| weight.number()));
        self.dynamic_css_props.insert(
            Cow::Borrowed(StyleName::FontWeight.into()),
            box_css_signal(weight),
        );
        self
    }

    /// Set the color.
    /// # Example
    /// ```no_run
    /// use zoon::{named_color::*, *};
    ///
    /// let button = Button::new()
    ///     .s(Font::new().color(GREEN_7))
    ///     .label("Click me");
    /// ```
    /// Set the color with macro.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let button = Button::new()
    ///     .s(Font::new().color(hsluv!(153.8, 99.1, 44.4)))
    ///     .label("Click me");
    /// ```
    pub fn color(mut self, color: impl IntoOptionColor) -> Self {
        if let Some(color) = color.into_option_color() {
            self.static_css_props
                .insert(StyleName::Color.into(), color.into_color_string());
        }
        self
    }

    /// Set color depending of signal's state.
    /// # Example
    /// ```no_run
    /// use zoon::{named_color::*, *};
    ///
    /// let (hovered, hover_signal) = Mutable::new_and_signal(false);
    /// let button = Button::new()
    ///     .s(Font::new().color_signal(hover_signal.map_bool(|| PINK_6, || BLUE_9)))
    ///     .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
    ///     .label("Hover me");
    /// ```
    pub fn color_signal(
        mut self,
        color: impl Signal<Item = impl IntoOptionColor> + Unpin + 'static,
    ) -> Self {
        let color = color.map(|color| color.into_option_color_string());
        self.dynamic_css_props.insert(
            Cow::Borrowed(StyleName::Color.into()),
            box_css_signal(color),
        );
        self
    }

    /// Set the font size.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let button = Button::new().s(Font::new().size(350)).label("Click me");
    /// ```
    pub fn size(mut self, size: u32) -> Self {
        self.static_css_props
            .insert(StyleName::FontSize.into(), px(size));
        self
    }

    /// Set font size depending of signal's state.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let (hovered, hover_signal) = Mutable::new_and_signal(false);
    /// let button = Button::new()
    ///     .s(Font::new().size_signal(hover_signal.map_bool(|| 350, || 150)))
    ///     .on_hovered_change(move |is_hovered| hovered.set_neq(is_hovered))
    ///     .label("Hover me");
    /// ```
    pub fn size_signal(
        mut self,
        size: impl Signal<Item = impl Into<Option<u32>>> + Unpin + 'static,
    ) -> Self {
        let size = size.map(|size| size.into().map(px));
        self.dynamic_css_props.insert(
            Cow::Borrowed(StyleName::FontSize.into()),
            box_css_signal(size),
        );
        self
    }

    /// Set the text line height in pixels.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let paragraph = Paragraph::new()
    ///     .s(Font::new().line_height(150))
    ///     .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
        self.static_css_props.insert(
            StyleName::LineHeight.into(),
            match line_height.into() {
                LineHeight::Px(pixels) => px(pixels),
                LineHeight::Normal => "normal".into_cow_str(),
            },
        );
        self
    }

    pub fn line_height_signal(
        mut self,
        line_height: impl Signal<Item = impl IntoOptionLineHeight> + Unpin + 'static,
    ) -> Self {
        let line_height = line_height.map(|line_height| {
            line_height
                .into_option_line_height()
                .map(|line_height| match line_height {
                    LineHeight::Px(pixels) => px(pixels),
                    LineHeight::Normal => "normal".into_cow_str(),
                })
        });
        self.dynamic_css_props.insert(
            Cow::Borrowed(StyleName::LineHeight.into()),
            box_css_signal(line_height),
        );
        self
    }

    /// Set the font as Italic.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use zoon::*;
    ///
    /// let paragraph = Paragraph::new()
    ///     .s(Font::new().italic())
    ///     .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn italic(mut self) -> Self {
        self.static_css_props
            .insert(StyleName::FontStyle.into(), "italic");
        self
    }

    /// Don't wrap the text according to its parent boundaries.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use zoon::*;
    ///
    /// let paragraph = Paragraph::new()
    ///     .s(Font::new().no_wrap())
    ///     .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn no_wrap(mut self) -> Self {
        self.static_css_props
            .insert(StyleName::WhiteSpace.into(), "pre");
        self
    }

    /// Set the text to be wrapped withing its element and prevent overflow.
    ///
    /// More information at <https://developer.mozilla.org/en-US/docs/Web/CSS/word-break>.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use zoon::*;
    ///
    ///  let paragraph =  Paragraph::new()
    ///     .s(Width::exact(50))
    ///     .s(Font::new().wrap_anywhere())
    ///     .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn wrap_anywhere(mut self) -> Self {
        // @TODO replace with the line below once `overflow-wrap: anywhere` works on
        // Safari https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-wrap#browser_compatibility
        self.static_css_props
            .insert(StyleName::WordBreak.into(), "break-word");
        // self.static_css_props.insert("overflow-wrap", "anywhere");

        self.static_css_props.remove(StyleName::WhiteSpace.into());
        self
    }

    /// Horizontally center the text inside its element.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let paragraph = Paragraph::new()
    ///    .s(Font::new().center())
    ///    .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn center(mut self) -> Self {
        self.static_css_props
            .insert(StyleName::TextAlign.into(), "center");
        self
    }

    pub fn left(mut self) -> Self {
        self.static_css_props
            .insert(StyleName::TextAlign.into(), "left");
        self
    }

    pub fn right(mut self) -> Self {
        self.static_css_props
            .insert(StyleName::TextAlign.into(), "right");
        self
    }

    pub fn justify(mut self) -> Self {
        self.static_css_props
            .insert(StyleName::TextAlign.into(), "justify");
        self
    }

    pub fn align_signal(
        mut self,
        alignment: impl Signal<Item = impl Into<Option<FontAlignment>>> + Unpin + 'static,
    ) -> Self {
        let alignment = alignment.map(|alignment| {
            alignment.into().map(|alignment| match alignment {
                FontAlignment::Center => "center",
                FontAlignment::Left => "left",
                FontAlignment::Right => "right",
                FontAlignment::Justify => "justify",
            })
        });
        self.dynamic_css_props.insert(
            <&str>::from(StyleName::TextAlign).into(),
            box_css_signal(alignment),
        );
        self
    }

    /// Set the family font with an array of [FontFamily].
    /// It is recommended to have few family fonts as
    /// fallback if the first ones are not available for web
    /// as documented here: <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
    ///
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let paragraph = Paragraph::new()
    ///    .s(Font::new().family([
    ///     FontFamily::new("Helvetica Neue"),
    ///     FontFamily::new("Helvetica"),
    ///     FontFamily::new("Arial"),
    ///     FontFamily::SansSerif,
    ///    ]))
    ///    .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn family(mut self, family: impl IntoIterator<Item = FontFamily<'a>>) -> Self {
        let family = family
            .into_iter()
            .map(|family| family.into_cow_str())
            .collect::<Cow<_>>()
            .join(", ");
        if family.is_empty() {
            return self;
        }
        self.static_css_props
            .insert(StyleName::FontFamily.into(), family);
        self
    }

    pub fn family_signal(
        mut self,
        family: impl Signal<Item = impl IntoIterator<Item = FontFamily<'a>>> + Unpin + 'static,
    ) -> Self {
        let family = family.map(|family| {
            let family_style = family
                .into_iter()
                .map(|family| family.into_cow_str())
                .collect::<Cow<_>>()
                .join(", ");
            if family_style.is_empty() {
                None?;
            }
            Some(family_style)
        });
        self.dynamic_css_props.insert(
            <&str>::from(StyleName::FontFamily).into(),
            box_css_signal(family),
        );
        self
    }

    /// Add underlining for the text with specific styling.
    /// [FontLine] has many options available.
    /// # Example
    /// ```no_run
    /// use zoon::*;
    ///
    /// let paragraph = Paragraph::new()
    ///     .s(Font::new().line(FontLine::new().underline().dashed()))
    ///     .content("Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...");
    /// ```
    pub fn line(mut self, line: FontLine<'a>) -> Self {
        self.static_css_props
            .extend(line.static_css_props.into_iter());
        self.dynamic_css_props
            .extend(line.dynamic_css_props.into_iter());
        self
    }

    pub fn tracking(mut self, tracking: i32) -> Self {
        self.static_css_props
            .insert(StyleName::LetterSpacing.into(), px(tracking));
        self
    }
}

impl<'a> Style<'a> for Font<'a> {
    fn move_to_groups(self, groups: &mut StyleGroups<'a>) {
        groups.update_first(|mut group| {
            let Self {
                static_css_props,
                dynamic_css_props,
            } = self;
            group.static_css_props.extend(static_css_props);
            group.dynamic_css_props.extend(dynamic_css_props);
            group
        });
    }
}
